PyGame is a simple and relatively easy to use library for writing games in Python.
It’s based on an old and very successful system for graphics, media, and games writing called SDL (https://www.libsdl.org/).
Here’s a small, minimal example of using PyGame that actually loads up a 600 x 480 window and makes the entire window white
import pygame
pygame.init()
size = [600,480]
screen = pygame.display.set_mode(size)
screen.fill((255,255,255))
pygame.display.flip()
That’s not a ton of code but that’s also not trivial. Let’s go through it line by line!
The first thing you have to do is load the pygame
library in the first place! There can be a ton of libraries installed on your system that you might want to be use so not all of them can be loaded at once. So, instead, you need to tell the Python interpreter that it needs to look for the library and load all of its code before continuing on.
import pygame
The next line is something that’s required by PyGame itself to do all the initial setup and communication with the hardware in order to be able to do things like display images or play sound.
pygame.init()
After that, we have actually to setup a screen by using pygame.display.set_mode
to set the size of the window and create it
size = [600,480]
screen = pygame.display.set_mode(size)
Here we’re using a type of Python data called a list that allows us to group data together.
Finally, we have these last two lines where we say the screen should be white and then actually display.
screen.fill((255,255,255))
pygame.display.flip()
Y’see, up to the point of pygame.display.flip()
nothing is actually displayed on the screen. Most computer graphics today is done via buffering, where all the changes to the screen are calculated in advance and then applied all at once rather than going through all the work of changing the actual display over and over again.
Now, you might notice this code only creates a window for a few moments before it then closes. That’s no good!
How do we actually get our window to stay open? That involves introducing a few new concepts. The first one of them is the idea of a loop or iteration. The basic concept is that if you want to do something over and over again until something changes, you use a “while loop”. In this case, we’re going to introduce a loop that is going to keep running and displaying to the screen.
import pygame
pygame.init()
size = [600,480]
screen = pygame.display.set_mode(size)
while True:
screen.fill((255,255,255))
pygame.display.flip()
Uhhh there’s a problem here though. You can’t actually close this program properly. It’s done a good job of staying open, but now it’s just staying open forever!
The final piece is that we need to listen for an event. We need to check to see if the user has attempted to close the window. This is going to be an event called pygame.QUIT
.
So, now, we’re going to create a variable called done
that we’ll use to keep track of whether a QUIT
even has happened. We are going to use a different kind of loop, a “for loop”, to check all the events that have happened since the loop last ran. We’re going to use an “if statement” to check if an event was a QUIT
event, and if it was we’ll change done
to True
.
import pygame
pygame.init()
size = [600,480]
screen = pygame.display.set_mode(size)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
screen.fill((255,255,255))
pygame.display.flip()
So we’ve managed to make a really simple program that
- opens a window
- displays something on it
- responds to events, in this case a
QUIT
event
All things considered, that’s a good bit of what you need to know! A lot of what we’re going to do in the rest of this tutorial is going to expand on the ideas of displaying things to the screen and responding to events.
There’s a couple of important concepts though: surfaces & rectangles.
A surface is the basic way that PyGame (and its underlying library, SDL) handle data that’s ready to display. There’s always at least one surface in a game, which is the one created when you call pygame.display.set_mode
. In our example above, we called this surface screen
. This is the surface that gets rendered every time we call pygame.display.flip
.
There’ll be other surfaces though, like the ones made when you load an image or render some text onto the screen.
The following is an example of some code where the text on the screen updates with every letter you type. Every time the loop runs, the program is going to render a surface consisting of all the text in the variable text
. This surface is stored in the variable textSurface
. It then combines the text-surface with the main surface for the screen using the blit
function. The blit
function takes a surface as an argument and the upper-left corner to render the surface too. In this case, it’ll be at the x/y coordinates of (100,100)
import pygame
pygame.init()
size = [600,480]
screen = pygame.display.set_mode(size)
done = False
text = ""
alphabet = "abcdefghijklmnopqrstuvwxzy"
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
elif event.type == pygame.KEYDOWN:
key = pygame.key.name(event.key).lower()
if key in alphabet:
text = text + key
elif event.key == pygame.K_BACKSPACE:
text = text[0:len(text)-1]
screen.fill((255,255,255))
font = pygame.font.SysFont('Arial', 25)
textSurface = font.render(text,True,(0,0,0))
screen.blit(textSurface,[100,100])
pygame.display.flip()
Similarly, we can render images to the screen from files like this example
import pygame
pygame.init()
size = [600,480]
screen = pygame.display.set_mode(size)
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
screen.fill((255,255,255))
image = pygame.image.load("thumb.png")
image = pygame.transform.scale(image,(100,100))
screen.blit(image,[200,200])
pygame.display.flip()
The other concept that we’ve been implicitly using this whole time is the rect, short for “rectangle”. In PyGame, a Rect
is a kind of primitive data that lays out a rectangular area for a few different uses:
- drawing onto a surface
- determining if two sprites are intersecting
To draw a simple rectangle is easy:
import pygame
pygame.init()
size = [640, 480]
screen = pygame.display.set_mode(size)
white = (255,255,255)
blue = (0,0,255)
playing = True
#rectangles are objects, which means that you make them
#with the constructor provided by PyGame
#To make a rectangle you provide the:
# x-coordinate of the upper left corner
# y-coordinate of the upper left corner
# width
# height
# Note that you're not giving it a color or anything like that, just shape
rec = pygame.Rect([320,240,100,100])
while playing:
for event in pygame.event.get():
if event.type ==pygame.QUIT:
playing = False
screen.fill(white)
#drawing a rectangle is done with the pygame.draw.rect function
#you give it the pygame surface to draw on, a color, and the rectangle object to draw
pygame.draw.rect(screen,blue,rec)
pygame.display.flip()
To move a rectangle, you have a few options. If you want to actually move an object on screen, probably the easiest to use would be the rect.move_ip
function. The “ip” in this context meaning “in place”, which means that it actually changes the rectangle itself rather than returning a copy of the rectangle with the position changed.
You can modify your above code so that it whenever you press one of the WASD keys it moves the rectangle
import pygame
pygame.init()
size = [640, 480]
screen = pygame.display.set_mode(size)
white = (255,255,255)
blue = (0,0,255)
playing = True
rec = pygame.Rect([320,240,100,100])
while playing:
for event in pygame.event.get():
if event.type ==pygame.QUIT:
playing = False
#Here we test for the pygame.KEYDOWN event
#If this event fires, then we should see *what* key fired
#We test to see if the key was one of WASD and move the rect accordingly if it is
#for the move_ip function the first argument is how much to change the x-coordinate
#and the second is how much to change the y-coordinate
elif event.type ==pygame.KEYDOWN:
if event.key == pygame.K_a:
rec.move_ip(-10,0)
elif event.key == pygame.K_d:
rec.move_ip(10,0)
elif event.key == pygame.K_s:
rec.move_ip(0,10)
elif event.key == pygame.K_w:
rec.move_ip(0,-10)
screen.fill(white)
pygame.draw.rect(screen,blue,rec)
pygame.display.flip()
Sprites in PyGame are represented by objects. You’re not ever going to explicitly construct an object using the Sprite constructor, though.
Instead, you’re going to make your own class that inherits from the pygame.sprite.Sprite class to represent each kind of entity in your game: platforms, player controlled characters, enemy characters, etc.
Here’s an example that uses the thumbs up image from above:
import pygame
pygame.init()
size = [640,480]
screen = pygame.display.set_mode(size)
white = (255,255,255)
running = True
class Thumb(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
im = pygame.image.load("thumb.png")
self.image = pygame.transform.scale(im,(100,100))
self.rect = self.image.get_rect()
thumb = Thumb()
spriteList = pygame.sprite.Group()
spriteList.add(thumb)
clock = pygame.time.Clock()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_w:
thumb.rect.move_ip(0,-10)
elif event.key == pygame.K_s:
thumb.rect.move_ip(0,10)
elif event.key == pygame.K_a:
thumb.rect.move_ip(-10,0)
elif event.key == pygame.K_d:
thumb.rect.move_ip(10,0)
screen.fill(white)
spriteList.draw(screen)
pygame.display.flip()
clock.tick(60)
Using sprite sheets is just an extension of using sprites but the image is slightly more complicated.
Basically, your sprite sheet contains all the sprites you want to use on one image seperated by space.
So now our example is going to look like:
import pygame
pygame.init()
size = [640,480]
screen = pygame.display.set_mode(size)
white = (255,255,255)
running = True
clock = pygame.time.Clock()
class Flapper(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.sheet = pygame.image.load("FlappySpritesheet.png")
self.image = pygame.Surface((100,100)).convert()
self.image.set_colorkey((0,0,0))
self.image.blit(self.sheet,(0,0),[0,0,100,100])
self.rect = self.image.get_rect()
self.tick = 0
def update(self):
self.tick = self.tick + 1
self.image = pygame.Surface((100,100)).convert()
self.image.set_colorkey((0,0,0))
if self.tick % 2 == 1:
self.image.blit(self.sheet,(0,0),[0,100,100,100])
else:
self.image.blit(self.sheet,(0,0),[0,0,100,100])
flapper = Flapper()
spriteList = pygame.sprite.Group()
spriteList.add(flapper)
clock = pygame.time.Clock()
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_w:
flapper.rect.move_ip(0,-10)
elif event.key == pygame.K_s:
flapper.rect.move_ip(0,10)
elif event.key == pygame.K_a:
flapper.rect.move_ip(-10,0)
elif event.key == pygame.K_d:
flapper.rect.move_ip(10,0)
screen.fill(white)
spriteList.update()
spriteList.draw(screen)
pygame.display.flip()
clock.tick(20)
There’s a few key ideas here.
The first is that of a spritesheet.
The actual image of our little flappy creature is going to be
You can see that there’s actually two images on this spritesheet representing the two frames of animation for the sprite. To actually animate a sprite we need to rapidly switch between them.
To do that, we’re going to define an update function that will be used to keep changing which part of the spritesheet is going to be copied over. Every time the update function is called, we add one to our “tick” counter. If the counter is odd, we use the second frame of animation. If it’s even, we use the first frame of animation.
Pong is kind of the “hello world” of games writing simply because, well, it was one of the first video games.
If you haven’t seen it before it’s https://upload.wikimedia.org/wikipedia/commons/f/f8/Pong.png
and it’s a simple table-tennis type of game.
So we’re going to create a really simple Pong clone
import pygame
import random
pygame.init()
pygame.mixer.init()
#helper function for choosing random ball velocities to start
def randomVel():
v = random.randrange(-5,5)
while v == 0:
v = random.randrange(-5,5)
return v
screenHeight = 480
screenWidth = 600
screen = pygame.display.set_mode((screenWidth,screenHeight))
done = False
playerYPos = screenHeight / 2
enemyYPos = screenHeight / 2
playerRect = pygame.Rect(50,screenHeight / 2 - 30, 20, 60)
enemyRect = pygame.Rect(500,screenHeight / 2 - 30, 20, 60)
ballRect = pygame.Rect(screenWidth/2,screenHeight/2,10,10)
ballVel = [randomVel(),randomVel()]
playerScore = 0
enemyScore = 0
backgroundColor = (0,0,0)
blockColor = (255,255,255)
pygame.key.set_repeat(50,50)
clock = pygame.time.Clock()
font = pygame.font.SysFont('Arial', 50)
pygame.mixer.music.load("bgm.mp3")
pygame.mixer.music.play(-1)
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
elif event.type == pygame.KEYDOWN:
key = event.key
if key == pygame.K_UP:
playerRect = playerRect.move(0,-15)
elif key == pygame.K_DOWN:
playerRect = playerRect.move(0,15)
if ballRect.y < enemyRect.top:
enemyRect = enemyRect.move(0,-5)
elif ballRect.y > enemyRect.bottom:
enemyRect = enemyRect.move(0,5)
screen.fill(backgroundColor)
#displayScore
pscore = font.render(str(playerScore),True,blockColor)
escore = font.render(str(enemyScore),True,blockColor)
screen.blit(pscore,[50,50])
screen.blit(escore,[500,50])
#move ball and check collisions
ballRect = ballRect.move(ballVel[0],ballVel[1])
if ballRect.bottom > screenHeight or ballRect.top < 0:
ballVel[1] = -ballVel[1]
elif ballRect.left < 0:
enemyScore = enemyScore + 1
ballRect.center = (screenWidth / 2, screenHeight / 2)
ballVel = [randomVel(),randomVel()]
elif ballRect.right > screenWidth:
playerScore = playerScore + 1
ballRect.center = (screenWidth / 2, screenHeight / 2)
ballVel = [randomVel(),randomVel()]
elif ballRect.colliderect(playerRect):
ballVel[0] = -ballVel[0]
elif ballRect.colliderect(enemyRect):
ballVel[0] = -ballVel[0]
pygame.draw.rect(screen,blockColor, ballRect)
# draw player
pygame.draw.rect(screen,blockColor, playerRect)
# draw enemy
pygame.draw.rect(screen,blockColor, enemyRect)
pygame.display.flip()
clock.tick(60)